package wortubung.lang;

import static java.lang.Math.max;
import static wortubung.util.MatrixPrinter.*;
import static wortubung.util.CommonUtils.*;
import static wortubung.util.JSONUtils.*;

import java.io.File;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;

import wortubung.JSONConsts;
import wortubung.Messages;
import wortubung.WortUbung;
import wortubung.WortUbungException;
import wortubung.core.EXAMPLE;
import wortubung.core.FixedUsage;
import wortubung.core.IDIOM;
import wortubung.core.Item;
import wortubung.core.LANG;
import wortubung.core.LOCUTION;
import wortubung.core.MEANING;
import wortubung.core.NounCategory;
import wortubung.core.POS;
import wortubung.core.POSCategory;
import wortubung.core.POSInfo;
import wortubung.core.SLANG;
import wortubung.core.WORD;
import wortubung.inflection.CONJUGATION;
import wortubung.inflection.InflectionDisplayFormat;
import wortubung.inflection.ConjugationResult;
import wortubung.inflection.ConjugationSet;
import wortubung.inflection.PERSON;
import wortubung.inflection.UnknownConjugationException;
import wortubung.inflection.VerbConjugation;
import wortubung.inflection.VerbConjugationFromSource;
import wortubung.lang.fr.Conjugueur;
import wortubung.util.CommonUtils;
import wortubung.util.MatrixPrinter;
import wortubung.util.MatrixPrinter.Matrix;
import wortubung.util.MatrixPrinter.STYLE;
import wortubung.util.Printable;
import wortubung.util.PrintableDelegate;
import wortubung.util.ToolBase;
import wortubung.util.TransliterateException;
import wortubung.wordlist.PrintOptions;
import wortubung.wordlist.WordListInfo;

/**
 * The interactive shell should initialize language managers with WortUbung.
 * For JUnit, it can be ToolBase.
 */
public abstract class LanguageManager extends PrintableDelegate {

	public static final String RESOURCE_BUNDLE = "wortubung/WortUbungMessages";

	// Reside in each language's folder in the resources, such as "de/builtin-lexicon.txt", etc.
	public static final String BUILTIN_LEXICON_FILE = "builtin-lexicon.txt";

	// French resource files
	public static final String FR_PRECONJUGUES_FILE          = "fr/preconjugues.fr.txt";
	public static final String FR_CONJUGAISONS_BY_RULE_FILE  = "fr/conjugaisons-by-rule.fr.json";
	public static final String FR_CONJUGAISONS_AS_IS_FILE    = "fr/conjugaisons-as-is.fr.json";
	public static final String FR_EXCEPTIONS_VERBE_FILE      = "fr/exceptions-verbe.fr.properties";

	// Spanish resource files
	public static final String ES_CONJUGACIONES_AS_IS_FILE   = "es/conjugaciones-as-is.es.json";
	public static final String ES_EXCEPCIONES_VERBALES_FILE  = "es/excepciones-verbales.es.properties";

	// Italian resource files
	public static final String IT_CONIUGAZIONI_AS_IS_FILE    = "it/coniugazioni-as-is.it.json";
	public static final String IT_EXCEPCIONES_VERBALES_FILE  = "it/eccezioni-verbali.it.properties";

	// German resource files
	public static final String DE_KONJUGATIONEN_AS_IS_FILE   = "de/Konjugationen-as-is.de.json";
	public static final String DE_KONJUGATIONEN_BY_RULE_FILE = "de/Konjugationen-by-rule.de.json";
	public static final String DE_NOMINA_FILE                = "de/Nomina.de.txt";
	public static final String DE_PREFIXES_FILE              = "de/prefixes.txt";
	public static final String DE_ADJEKTIVUNDADVERB_FILE     = "de/AdjektivUndAdverb.de.properties";
	public static final String DE_PRONOMARTIKELUSW_FILE      = "de/PronomArtikelUsw.de.json";

	private static LanguageManager[] lms = null;

	private VerbConjugationFromSource externalConjugator;

	public static LanguageManager[] getAllManagers(Printable prompt) {
		if (lms == null)
			lms = new LanguageManager[] {
					new LangFrenchManager (prompt),
					new LangGermanManager (prompt),
					new LangSpanishManager(prompt),
					new LangItalianManager(prompt),
					new LangRussianManager(prompt),
//					new LangGreekManager  (prompt),
//					new LangLatinManager  (prompt),
					new LangEnglishManager(prompt)
			};

		return lms;
	}

	public static LanguageManager get(LANG lang) {
		for (LanguageManager lm : lms)
			if (lm.getLanguage() == lang)
				return lm;
		return null;
	}

	protected LanguageManager(Printable dict) { this(dict, null); }

	protected LanguageManager(Printable dict, VerbConjugationFromSource externalConjugator) {
		super(dict);
		this.externalConjugator = externalConjugator;
		reset();
	}

	public abstract LANG getLanguage();
	public abstract Collection<String> getVerbsOfIrregularConjugations();
	public abstract VerbConjugation conjugate(String verb);

	public boolean hasExternalConjugator() { return externalConjugator != null; }
	public ConjugationResult getConjugationFromSource(String verb) throws IOException {
		return externalConjugator.getResult(verb);
	}

	public WortUbung getWortUbung() { return (WortUbung)getPrintable(); }
	public ToolBase getToolBase() { return (ToolBase)getPrintable(); }
    public int getPageWidth() { return getWortUbung().getPageWidth(); }
    public PrintStream getLog() { return ToolBase.log; }
    public String getLangName() { return getLanguage().getLongName(); }

	public void conjugate(String params, InflectionDisplayFormat format, boolean fromExternalConjugator) {
		if (fromExternalConjugator && !hasExternalConjugator()) {
			promptln(Messages.NO_EXTERNAL_CONJUGATOR, getLangName());
			return;
		}
    	ArrayList<VerbConjugation> conjs = new ArrayList<VerbConjugation>();
    	String[] sa = params.split(" +");
    	trim(sa);
    	try {
			toVernacular(sa);
		} catch (TransliterateException e) {
			promptln(e.getMessage());
			return;
		}
    	for (String verb : sa) {
    		if (isNotEmpty(verb)) {
    			try {
    				VerbConjugation vc = fromExternalConjugator ? getConjugationFromSource(verb) : conjugate(verb);
    				if (vc != null)
    					conjs.add(vc);
    			} catch(UnknownConjugationException uce) {
    				promptln(Messages.NOT_KNOW_CONJUGATE, verb);
    			} catch (IOException e) {
    				promptln(Messages.FAILED_EXTERNAL_CONJUGATE);
					e.printStackTrace();
				}
    		}
    	}

    	if (conjs.size() > 0) {
	    	if (format == InflectionDisplayFormat.JSON)
	    		printConjugationsJSON(conjs);
	    	else
	    		printConjugations(conjs);
    	}
	}

    public void unconjugate(String params) {
    	String[] words;
		try {
			words = toVernacular(params).split(" +");
		} catch (TransliterateException e) {
			promptln(e.getMessage());
			return;
		}
    	int len = maxLen(4, words);
    	String fmt = "%-" + len + "s - %s: %s";
    	for (String w : words) {
        	List<ReverseConjugationElement> ces = conjugatedMap.get(w);
        	if (length(ces) > 0) {
        		String x = null;
        		for (int i=0; i<ces.size(); ++i) {
        			ReverseConjugationElement ce = ces.get(i);
        			if (i == 0)
        				x = String.format(fmt, w, ce.getVerb(), ce.getDescription());
        			else
        				x += ", " + ce.getDescription();
        		}
        		println(x);
        	}
    	}
    	println();
    }
    
    public void toVernacular(String[] sa) throws TransliterateException {
    	for (int i=length(sa)-1; i>=0; --i)
    		sa[i] = toVernacular(sa[i]);
    }

    public String toVernacular(String transliterate) throws TransliterateException {
    	return getLanguage().toVernacular(transliterate);
    }

    public void toLatin(String[] sa, boolean withDiacritics) {
    	for (int i=length(sa)-1; i>=0; --i)
    		sa[i] = toLatin(sa[i], withDiacritics);
    }

    public String toLatin(String transliterate, boolean withDiacritics) {
    	return getLanguage().toLatin(transliterate, withDiacritics);
    }

    public void findAndShowWords(String params) {
    	try {
			new WordCollection(this).findAndShowWords(preprocessWordPattern(params));
		} catch (TransliterateException e) {
			promptln(e.getMessage());
		}
    }
    
    protected String preprocessWordPattern(String pattern) throws TransliterateException {
    	pattern = toVernacular(pattern);
    	return toLatin(pattern, false).replaceAll("_", " ").replaceAll("\\*", ".*");
    }

    public void findAndShowWordsByDescription(String params) {
    	if (isEmpty(params) || params.trim().equals("*"))
    		return;
    	if (!params.startsWith("*"))
    		params = "*" + params;
    	if (!params.endsWith("*"))
    		params += "*";
    	params = params.replaceAll("_", " ").replaceAll("\\*", ".*");
    	new WordCollection(this).findAndShowWordsByDescription(params);
    }

    ////////////////////////////////////////////////////////////////////////////////////////
	// Language knowledge
	
    public void decline(String params) {
    	promptln(Messages.NOT_KNOW_DECLINE, params, getLangName());
    }

    public void comparative(String params) {
    	promptln(Messages.NOT_KNOW_COMPARATIVE, params, getLangName());
    }

    public String numberInWords(int integral) {
    	return numberInWords(integral, null, false);
    }

    public String numberInWords(int integral, boolean isOrdinal) {
    	return numberInWords(integral, null, isOrdinal);
    }

    public String numberInWords(int integral, String fraction, boolean isOrdinal) {
    	throw new RuntimeException("NEEDED: numberInWords() is not implemented for " + getLangName());
    }

    public String dateInWords(int year, int month, int day) {
    	throw new RuntimeException("NEEDED: dateInWords() is not implemented for " + getLangName());
    }

    public String timeInWords(int hr, int min, int sec) {
    	throw new RuntimeException("NEEDED: timeInWords() is not implemented for " + getLangName());
    }

    ////////////////////////////////////////////////////////////////////////////////////////
	// Dictionary

	public static final String LATEST_WORDS_FILENAME = "latest_words.";

	protected HashSet<WORD> words;
    protected HashMap<String,Object> wordMap; // value: WORD or WORD[]
    protected ArrayList<WordListInfo> wordLists;
    protected int maxWordListShortNameLen;

    private transient HashMap<String, String> wordsInLists; // word => listName
	private ArrayList<String> latestWords = new ArrayList<String>();
    private HashSet<String> latestWordsSet = new HashSet<String>();
    private int numPrevLatestWords = 0;

    public void reset() {
    	maxWordListShortNameLen = 0;
    	if (words == null) {
	        words = new HashSet<WORD>();
	        wordMap = new HashMap<String,Object>(); // value: WORD or WORD[]
	        wordsInLists = new HashMap<String, String>();
	        wordLists = new ArrayList<WordListInfo>();
    	} else {
	        words.clear();
	        wordMap.clear();
	        wordsInLists.clear();
	        wordLists.clear();
    	}
        loadBuiltinLexicon();
    }

    public String[] getListShortNames() {
    	String[] ret = new String[length(wordLists)];
    	for (int i=0; i<ret.length; ++i)
    		ret[i] = wordLists.get(i).getShortName();
    	return ret;
    }

    public HashMap<String,Object> getWordMap() { return wordMap; }

    public void showWord(WORD word, int idx) {
    	getToolBase().printLine("" + idx + ". [ " + word.getWord() + " ] ");
    }

    public Collection<WORD> getWords() { return words; }
    public List<WordListInfo> getWordLists() { return wordLists; }

    public void addWordList(WordListInfo wordListInfo) {
        wordLists.add(wordListInfo);
        if (wordListInfo.getShortName().length() > maxWordListShortNameLen)
            maxWordListShortNameLen = wordListInfo.getShortName().length();
    }

    private void loadBuiltinLexicon() {
    	String fileName = getLanguage().name() + '/' + BUILTIN_LEXICON_FILE;
    	String content = null;
    	try {
			content = readFileFromCPAsString(fileName);
		} catch (IOException e) {
		}
    	if (content == null)
    		return;
    	for (String line : content.split("\n"))
    		if (isNotEmpty(line))
    			readBuiltinWordlist(line.trim());
    }
    
    private void readBuiltinWordlist(String fileName) {
    	LineNumberReader lnr = openFileFromCP(fileName);
    	if (lnr == null)
    		return;
    	WordListInfo wordListInfo;
		try {
			wordListInfo = parseWordListMetaLine(fileName, lnr);
			parseLineFile(wordListInfo, lnr);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try { lnr.close(); } catch(Exception e) {}
		}
    }

    public static WordListInfo parseWordListMetaLine(Object fileOrName, LineNumberReader lnr) throws IOException {
        String line = lnr.readLine(); // first line
        if (line != null)
        	line = line.trim();
    	int idx = line.indexOf("#");
        if (idx < 0)
            throw new WortUbungException("Wordlist file is not valid: no meta information found.");
        line = line.substring(idx+1);

        LANG lang = null;
    	String name = null;
    	String shortName = null;
		for (String s : line.split(",")) {
			String[] sa = s.split("=");
			if (sa.length == 2) {
				s = sa[0].trim();
				String v = sa[1].trim();
				if ("lang".equals(s))
					lang = LANG.valueOf(v);
				else if ("name".equals(s))
					name = v;
				else if ("shortname".equalsIgnoreCase(s))
					shortName = v;
			}
    	}

		if (fileOrName instanceof File)
			return new WordListInfo((File)fileOrName, lang, name, shortName);
        return new WordListInfo((String)fileOrName, lang, name, shortName);
    }

    public void parseLineFile(WordListInfo wordListInfo, LineNumberReader lnr) throws IOException {
    	addWordList(wordListInfo);

    	String line;
    	ArrayList<String> lines = new ArrayList<String>();
        while ((line = lnr.readLine()) != null) {
            line = line.trim();
            if (line.length() == 0) {
                // Empty line terminates a word definition
                if (lines.size() > 0) {
                	try {
                        parseWordLines(lines);
                	} catch(WortUbungException wue) {
                		wue.setFileInfo(wordListInfo.getFileName(), lnr.getLineNumber()-1);
                		getLog().println("Error: " + wue.getMessage());
                	}
                    lines.clear();
                }
            } else if (line.startsWith("#")) {
                // ignore comment lines
                continue;
            } else {
                // Concatenate lines ending with '\'.
                while (line.endsWith("\\")) {
                    line = line.substring(0, line.length()-1).trim();
                    String tmp;
                    if ((tmp = lnr.readLine()) == null)
                        break;
                    line += " " + tmp.trim();
                }
                lines.add(line);
            }
        }
        if (lines.size() > 0) {
        	try {
        		parseWordLines(lines);
            } catch(WortUbungException wue) {
            	wue.setFileInfo(wordListInfo.getFileName(), lnr.getLineNumber()-1);
            	getLog().println("Error: " + wue.getMessage());
            }
        }
    }

    protected POSInfo parseInflection(String word, POSInfo posInfo, String inflection) {
    	// language-specific inflection/pos parsing.
    	return posInfo;
    }

    private WORD parseWordLine(String line, WORD rootWord) {
		int idx = line.indexOf(":::");
        if (idx < 0)
            throw new WortUbungException("Invalid line [" + line + "] - missing marker :::");

        // The first line: word [alias] @level ::: /pos[,pos]/ [pronounce] {plural} definition <EX> example <EX> example
        //           Where word and pos are required, all others are optional.
        String wordText = line.substring(0, idx).trim();
        line = line.substring(idx + 3).trim();
        String alias = null;
        int level = 0;
        idx = wordText.lastIndexOf('@');
        if (idx > 0) {
            level = Integer.parseInt(wordText.substring(idx+1).trim());
            wordText = wordText.substring(0, idx).trim();
        }
        idx = wordText.lastIndexOf('[');
        if (idx > 0 && wordText.endsWith("]")) {
            alias = wordText.substring(idx+1, wordText.length()-1).trim();
            wordText = wordText.substring(0, idx).trim();
        }

        String[] sa = null;
    	POSInfo[] posInfos = null;
        idx = line.indexOf('/', 1);
        if (line.startsWith("/") && idx >= 0) {
            sa = line.substring(1, idx).split(",");
            trim(sa);
            posInfos = new POSInfo[sa.length];
            for (int i=0; i<sa.length; ++i)
            	posInfos[i] = POSInfo.parsePOS(sa[i]);
        }
        if (idx >= 0)
        	line = line.substring(idx+1).trim();
        String pronounce = null;
        String inflection = null;
        while (line.startsWith("[") || line.startsWith("{")) {
            if (line.startsWith("[")) {
                idx = line.indexOf(']');
                pronounce = line.substring(1, idx).trim();
            } else if (line.startsWith("{")) {
                idx = line.indexOf('}');
                inflection = line.substring(1, idx).trim();
            }
            line = line.substring(idx+1);
        }

        if (posInfos == null)
        	throw new WortUbungException("Word '" + wordText + "' does not have POS set.");
        WordListInfo wordListInfo = wordLists.get(wordLists.size()-1);
        WORD wd = new WORD(wordListInfo, wordText, alias, posInfos[0], wordListInfo.getLang());
        wd.setPronounce(pronounce);
        wd.setInflection(inflection);
        wd.setLevel(level);

        posInfos[0] = parseInflection(wordText, posInfos[0], inflection);
        if (line.length() > 0)
            parseMeaning(wd, line, posInfos[0]);

        String listName = wordsInLists.get(wd.getWord());
        String listName1 = wd.getWordListInfo().getName();
        if (listName != null && !listName.equals(listName1))
        	throw new WortUbungException("Word '" + wd.getWord() + "' exists in both lists of '" + listName + "' and '" + listName1 + "'.");
        wordsInLists.put(wd.getWord(), listName1);

        wd.check();
        words.add(wd);
        if (rootWord != null)
        	rootWord.addDerivedWord(wd);

        Object x = wordMap.get(wd.getWord());
        if (x == null)
        	wordMap.put(wd.getWord(), wd);
        else if (x instanceof WORD)
        	wordMap.put(wd.getWord(), new WORD[]{ (WORD)x, wd });
        else if (x instanceof WORD[]) {
        	WORD[] oldArr = (WORD[])x;
        	WORD[] newArr = new WORD[oldArr.length + 1];
        	for (int i=0; i<oldArr.length; ++i)
        		newArr[i] = oldArr[i];
        	newArr[newArr.length-1] = wd;
        	wordMap.put(wd.getWord(), newArr);
        }

        return wd;
    }

    private void parseWordLines(ArrayList<String> lines) {
        WORD word = parseWordLine(lines.get(0), null); // first line must be a word

        for (int i=1; i<lines.size(); ++i) {
            String line = lines.get(i);
            int idx = line.indexOf("==>");
            int prefixLen = (idx >= 0) ? (idx+3) : -1;
            if (prefixLen < 0)
            	parseDefinition(word, line, word.getDefaultPOSInfo());
            else
           		parseWordLine(line.substring(prefixLen).trim(), word);
        }

        if (word.getDefaultPOS() == null)
            throw new WortUbungException("/pos/ is not set for word " + word.getWordWithAlias() + ".");
    }

    private void parseDefinition(WORD word, String origline, POSInfo defaultPosInfo) {
    	String[] sa = origline.split("\\$");
    	trim(sa);
        for (String line : sa) {
        	POS pos = null;
        	if (line.startsWith("/loc/")) {
        		pos = POS.LOCUTION;
        		line = line.substring(5);
        	}
        	else if (line.startsWith("/idiom/")) {
        		pos = POS.IDIOM;
        		line = line.substring(7);
        	}
        	else if (line.startsWith("/slang/")) {
        		pos = POS.SLANG;
        		line = line.substring(7);
        	}
        	else if (line.startsWith("/")) {
               	parseMeaning(word, line, null);
               	continue;
        	}
        	if (pos != null) {
        		FixedUsage fu = null;
            	int idx = line.indexOf(':');
            	if (idx < 0)
                    throw new WortUbungException("/" + pos.getShortName() + "/ has no expression because ':' is not found.");
            	String expr = line.substring(0, idx).trim();
            	switch(pos) {
            	case IDIOM:		fu = new IDIOM(word, expr); break;
            	case SLANG:		fu = new SLANG(word, expr); break;
            	case LOCUTION:	fu = new LOCUTION(word, expr); break;
            	}
            	parseMeaning(fu, line.substring(idx+1).trim(), word.getDefaultPOSInfo());
           		word.addFixedUsage(fu);
        	}
        }
    }

    private void parseMeaning(Item item, String line, POSInfo defaultPosInfo) {
    	POSInfo[] posInfos = null;
        int idx;
        if (line.startsWith("/")) {
            idx = line.indexOf('/', 1);
            if (idx < 0)
                throw new WortUbungException("/pos/ is not set correctly.");
            String[] sa = line.substring(1, idx).split(",");
            trim(sa);
            posInfos = new POSInfo[sa.length];
            for (int i=0; i<sa.length; ++i)
            	posInfos[i] = POSInfo.parsePOS(sa[i]);
            line = line.substring(idx+1);
        } else {
        	posInfos = new POSInfo[]{ defaultPosInfo };
        }
        idx = line.indexOf("<EX>");
        String line1 = null;
        if (idx >= 0) {
            line1 = line.substring(idx+4);
            line = line.substring(0, idx);
        }
        POSInfo posInfo = posInfos[0];
        MEANING m = new MEANING(item, line.trim(), posInfo);
        while (line1 != null) {
            idx = line1.indexOf("<EX>");
            if (idx > 0) {
                m.addExample(new EXAMPLE(line1.substring(0, idx).trim()));
                line1 = line1.substring(idx+4).trim();
            } else {
                m.addExample(new EXAMPLE(line1));
                line1 = null;
            }
        }
        item.addDefinition(m);
        for (int i=1; i<length(posInfos); ++i) {
        	m = m.clone(posInfos[i]);
        	item.addDefinition(m);
        }
    }

    public void doPrintWords(String params) {
    	try {
    		PrintOptions options = new PrintOptions(getWortUbung(), params);
        	if (options.isHelp()) {
       			options.showHelp();
       			return;
        	}
    		setPrinting(true);
        	openOutputFile(options.getOutputFileName());
        	if (options.isWordsOnly())
        		printWordsOnly(options);
        	else
        		printWords(options);
    	} catch(Exception e) {
    		error(e);
    	} finally {
    		setPrinting(false);
    		try {
    			closeOutputFile();
    		} catch(Exception e) {
        		error(e);
    		}
    	}
    }

    public void printWordsOnly(PrintOptions options) {
    	String[] words = options.getWords(getLanguage());
        println(concatEvenly(null, false, getPageWidth(), options.getWordWithAliasMaxLen(), null, words));
    }

    public void printWords(PrintOptions options) {
        WORD[] words = options.query(this, true);

        int digits = numDigits(length(words));
        String fmt = "%" + digits + "d%s";
        int idx = 2;
        boolean includeListName = options.includeListName();
        boolean includeLevel = options.includeLevel();
        boolean isPOSFullName = options.includeNounCategory();
        if (includeListName || includeLevel) {
        	fmt += '<';
            if (includeListName) {
            	fmt += "%-" + maxWordListShortNameLen + "s";
            	++idx;
            }
            if (includeListName && includeLevel)
            	fmt += '/';
            if (includeLevel) {
            	fmt += "%d";
            	++idx;
            }
            fmt += "> ";
        }
        if (!fmt.endsWith(" "))
        	fmt += ' ';

        if (!options.isComplete())
        	++idx;
        Object[] values = new Object[idx];

        idx = 1;
        for (WORD word : words) {
            int i=0;
            values[i++] = idx++;
            values[i++] = "."; // (word.getLevel() == 5) ? "-" : ".";
            if (options.includeListName())
            	values[i++] = word.getWordListInfo().getShortName();
            if (options.includeLevel())
            	values[i++] = word.getLevel();

            String prefix = String.format(fmt, values);
            String[] lines;
            boolean isComplete = options.isComplete();
            if (isComplete)
                lines = word.toCompleteInfoDisplay(prefix, options.getPageWidth(), isPOSFullName);
            else
            	lines = word.toQuickLine(0, prefix, options.getWordWithAliasMaxLen(), options.getPageWidth(), isPOSFullName);
            for (String line : lines)
            	println(line);
            if (isComplete)
            	println();
        }
        println();
    }

    public void showPrefixCounts(String params, boolean isFillBlank) {
		final int THREE_LIMIT = 10;
		final int FOUR_LIMIT  = 10;

		HashSet<WordListInfo> wordLists = getWordLists(params);

		HashSet<String> mustHave = new HashSet<String>();
		HashMap<String, Integer> three = new HashMap<String, Integer>();
		HashMap<String, Integer> four = new HashMap<String, Integer>();
		HashMap<String, Integer> five = new HashMap<String, Integer>();
		for (WORD wd : words) {
			if (wordLists != null && !wordLists.contains(wd.getWordListInfo()))
				continue;
			try {
				String s = wd.getWord().toLowerCase();
	        	if (s.length() >= 5)
	        		addCount(five, getLanguage().toVernacular(s.substring(0, 5)), 1);
	        	if (s.length() >= 4)
						addCount(four, getLanguage().toVernacular(s.substring(0, 4)), 1);
	        	if (s.length() >= 3)
	        		addCount(three, getLanguage().toVernacular(s.substring(0, 3)), 1);
	        	else
					mustHave.add(getLanguage().toVernacular(s));
			} catch (TransliterateException e) {
				e.printStackTrace();
			}
        }

		int cnt;
		HashMap<String, Integer> good = new HashMap<String, Integer>();
		for (String th : three.keySet()) {
			cnt = three.get(th);
			if (cnt <= THREE_LIMIT)
				good.put(th, cnt);
		}
		for (String fo : four.keySet()) {
			if (!good.containsKey(fo.substring(0, 3))) {
    			cnt = four.get(fo);
    			if (cnt <= FOUR_LIMIT)
    				good.put(fo, cnt);
			}
		}
		for (String fi : five.keySet()) {
			if (!good.containsKey(fi.substring(0, 3)) && !good.containsKey(fi.substring(0, 4)))
				good.put(fi, five.get(fi));
		}
		for (String mh : mustHave) {
			if (!good.containsKey(mh))
				good.put(mh, 1);
		}
		String[] prefixes = good.keySet().toArray(new String[good.size()]);
		Arrays.sort(prefixes);
		char cur = ' ';
		String curPrefix = null;
		int i = 0;
		if (isFillBlank) {
			String newPrefixIndicator = ":";
			int len = 15;
			int num = (getPageWidth() - 3) / (len + 1);
    		for (String p : prefixes) {
    			cnt = good.get(p);
    			boolean isNewPrefix = !p.equals(curPrefix);
    			if (isNewPrefix)
    				curPrefix = p;
    			p = makeStringLen(p, 0, len, '_');
    			if (isNewPrefix) {
    				cur = p.charAt(0);
    				if (i > 0)
    					println();
    				print((isNewPrefix ? newPrefixIndicator : " ") + p);
    				isNewPrefix = false;
    			}
    			p = " " + p;
    			for (int j=1; j<cnt; ++j) {
           			if (i > 0 && i % num == 0)
           				println();
           			++i;
        			print(p);
    				isNewPrefix = false;
    			}
    		}
		} else {
    		for (String p : prefixes) {
    			cnt = good.get(p);
    			if (p.charAt(0) != cur)
    				cur = p.charAt(0);
       			prefixes[i++] = String.format("%3d %-3s", cnt, p);
    		}
    		getWortUbung().printToPageWidth(prefixes, 1);
		}
		println();
		println();
	}

    public HashSet<WordListInfo> getWordLists(String params) {
    	HashSet<WordListInfo> ret = new HashSet<WordListInfo>();
    	HashSet<String> set = null;
    	if (isNotEmpty(params)) {
    		String[] sa = params.split(" +");
    		trim(sa);
    		for (String sn : sa) {
    			if (sn.startsWith("-"))
    				continue;
    			if (set == null)
    				set = new HashSet<String>();
    			if (sn.endsWith(","))
    				sn = sn.substring(0, sn.length()-1).trim();
    			set.add(sn);
    		}
    	}
    	for (WordListInfo list : wordLists) {
    		if (set == null || set.contains(list.getShortName()))
    			ret.add(list);
    	}
    	return ret;
    }

    /**
     * @return { builtin, loaded }
     */
	public int[] numberOfWordLists() {
		int[] ret = { 0, 0 };
		for (WordListInfo wli : getWordLists())
			++ret[wli.isBuiltin() ? 0 : 1];
		return ret;
	}

    public WordListInfo[] getWordListsAsArray(String params) {
    	WordListInfo[] arr = getWordLists(params).toArray(new WordListInfo[0]);
    	Arrays.sort(arr);
    	return arr;
    }

	public static void showStatsGeneral() {
		int i;
		Object[][] matrix = new Object[15][];
		String[] titles = new String[lms.length + 1];
		titles[0] = "";
		for (i=matrix.length-1; i>=0; --i)
			matrix[i] = new Object[lms.length + 1];
		matrix[ 0][0] = "Built-in word lists:";
		matrix[ 1][0] = "Loaded word lists:";
		matrix[ 2][0] = "Total words:";
		matrix[ 3][0] = "Words with single meaning:";
		matrix[ 4][0] = "Total meanings:";
		matrix[ 5][0] = "Total CJK meanings:";
		matrix[ 6][0] = "Total fixed usages:";
		matrix[ 7][0] = "Total examples:";
		matrix[ 8][0] = "Base irregular Verbs/Roots:";
		matrix[ 9][0] = "Nouns:";
		matrix[10][0] = "Verbs:";
		matrix[11][0] = "Adjectives:";
		matrix[12][0] = "Adverbs:";
		matrix[13][0] = "Conj/Prep/Prons:";
		matrix[14][0] = "Exclamations:";

		i = 1;
		for (LanguageManager lm : lms) {
			int wordCount = 0;
			int singleMeaningCount = 0;
			int meaningCount = 0;
			int meaningCJKCount = 0;
			int exampleCount = 0;
			int nounCount = 0;
			int adjCount = 0;
			int advCount = 0;
			int verbCount = 0;
			int exclCount = 0;
			int fixedUsageCount = 0;
			int otherPosCount = 0;
		
			for (WORD wd : lm.words) {
				++wordCount;
				exampleCount += wd.numExamples();
				fixedUsageCount += wd.numFixedUsages();
				meaningCJKCount += wd.numMeaningsCJK();
				int x = wd.numMeanings();
				meaningCount += x;
				if (x == 1)
					++singleMeaningCount;
				boolean[] typeCounts = wd.getMeaningTypes();
				if (typeCounts[0]) ++ nounCount;
				if (typeCounts[1]) ++ verbCount;
				if (typeCounts[2]) ++ adjCount;
				if (typeCounts[3]) ++ advCount;
				if (typeCounts[4]) ++ otherPosCount;
				if (typeCounts[5]) ++ exclCount;
			}
			int len = lm.getLangName().length();
			titles[i] = lm.getLangName();
			int[] counts = lm.numberOfWordLists();
			matrix[ 0][i] = nonzero(len, counts[0]);
			matrix[ 1][i] = nonzero(len, counts[1]);
			matrix[ 2][i] = nonzero(len, wordCount);
			matrix[ 3][i] = nonzero(len, singleMeaningCount);
			matrix[ 4][i] = nonzero(len, meaningCount);
			matrix[ 5][i] = nonzero(len, meaningCJKCount);
			matrix[ 6][i] = nonzero(len, fixedUsageCount);
			matrix[ 7][i] = nonzero(len, exampleCount);
			matrix[ 8][i] = nonzero(len, length(lm.getVerbsOfIrregularConjugations()));
			matrix[ 9][i] = nonzero(len, nounCount);
			matrix[10][i] = nonzero(len, verbCount);
			matrix[11][i] = nonzero(len, adjCount);
			matrix[12][i] = nonzero(len, advCount);
			matrix[13][i] = nonzero(len, otherPosCount);
			matrix[14][i] = nonzero(len, exclCount);
			++i;
		}
		MatrixPrinter.printMatrix(null, false, titles, matrix, true, false);
	}
	
	private static String nonzero(int width, int i) {
		return (i == 0) ? "" : rightFlush(width, String.valueOf(i));
	}

	public void showNounCategories() {
		HashMap<String,Integer> nounCategoryCounts = new HashMap<String,Integer>();
		for (WORD wd : words) {
			for (MEANING m : wd.getDefinition()) {
				if (m.getPOSCategory() == POSCategory.Noun) {
					NounCategory nc = m.getNounCategory();
					if (nc != null)
						incCount(nounCategoryCounts, nc.name().toLowerCase());
				}
			}
		}

		int i = 0;
		String[] sa = new String[NounCategory.values().length];
		for (NounCategory nc : NounCategory.values())
			sa[i++] = nc.name().toLowerCase();
		Arrays.sort(sa);
		prompt("    Noun category counts:");
		i = 0;
		for (String s : sa) {
			if (i++ % 6 == 0) {
				promptln();
				prompt("      ");
			}
			Integer cnt = nounCategoryCounts.get(s);
			if (cnt == null)
				cnt = 0;
			prompt(String.format("%s:%3d     ", s, cnt));
		}
		promptln("\n");
	}

	public void showListPrefix(String params) {
    	String[] prefices = null;
    	if (isNotEmpty(params))
    		prefices = params.split(" +");
    	Map<String, Set<String>> prefixToList = getWordListsForShortNames(prefices);
    	if (prefices == null)
    		prefices = prefixToList.keySet().toArray(new String[prefixToList.size()]);
    	Arrays.sort(prefices);

    	ArrayList<Object> list = new ArrayList<Object>();
    	for (String prefix : prefices) {
    		Set<String> set = prefixToList.get(prefix);
    		if (set == null)
    			continue;
    		String[] sa = set.toArray(new String[set.size()]);
    		Arrays.sort(sa);
    		list.add(prefix + " - " + concat(',', sa));
    	}
    	getWortUbung().printToPageWidth(list, 2);
    }

    public Map<String, Set<String>> getWordListsForShortNames(String[] params) {
    	int prefixLen = 3;
    	String[] prefices = null;
    	Set<String> shortNames = null;
    	if (isNotEmpty(params)) {
    		prefices = params;
    		for (String pref : prefices) {
    			int len = pref.length();
    			if (len > 1 && len < prefixLen)
    				prefixLen = len;
    		}
    		for (int i=prefices.length-1; i>=0; --i) {
    			if (prefices[i].length() > prefixLen)
    				prefices[i] = prefices[i].substring(0, prefixLen);
    			if (shortNames == null)
    				shortNames = new HashSet<String>();
    			shortNames.add(prefices[i]);
    		}
    	}

    	HashMap<String, Set<String>> prefixToList = new HashMap<String, Set<String>>();
		Set<String> set;
    	for (WordListInfo list : wordLists) {
    		for (String pref : list.getPrefices(prefixLen)) {
    			if (shortNames != null && !shortNames.contains(pref))
    				continue;
    			set = prefixToList.get(pref);
    			if (set == null) {
    				set = new HashSet<String>();
    				prefixToList.put(pref, set);
    			}
    			set.add(list.getShortName());
    		}
    	}
    	return prefixToList;
    }

	public void showListInfo(String params) {
    	WordListInfo[] wordlists = getWordListsAsArray(params);
		boolean isCSV = isNotEmpty(params) && params.trim().endsWith("-csv");
		if (isCSV) {
        	for (WordListInfo list : wordlists)
        		println(list.getShortName() + ',' + list.numberOfOriginalWords() + ',' + list.numberOfAllWords());
		} else {
			println("(The first three columns are: list short name, original word counts, and all word counts.)\n");
			String fmt = "%6s  %5d  %5d  %s";
			int totalOrig = 0;
			int totalList = 0;
			StringBuilder sb = new StringBuilder();
        	for (WordListInfo list : wordlists) {
    			Map<String,Integer> prefixCnts = list.getPrefixCounts(2);
    			String[] sa = prefixCnts.keySet().toArray(new String[prefixCnts.size()]);
    			Arrays.sort(sa);
    			sb.setLength(0);
    			boolean start = true;
    			for (String pref : sa) {
    				if (start)
    					start = false;
    				else
    					sb.append(", ");
    				sb.append(pref);
    				appendWithPrefixSuffix(sb, "(", prefixCnts.get(pref), ")");
    			}
    			int numOrig = list.numberOfOriginalWords();
    			int numList = list.numberOfAllWords();
    			totalOrig += numOrig;
    			totalList += numList;
    			println(String.format(fmt, list.getShortName(), numOrig, numList, sb));
        	}
    		println("------  -----  -----");
    		println(String.format(fmt, "TOTAL:", totalOrig, totalList, ""));
        	println();
    	}
    }

	public void doInspectWordsInLists(String params) {
    	ArrayList<String> withIdenticalMeaning = new ArrayList<String>();
    	ArrayList<String> includedInMeaning = new ArrayList<String>();
    	int maxLen = 8;
    	String w;
    	for (WordListInfo wli : getWordListsAsArray(params)) {
lp:    		for (WORD wd : wli.getWords()) {
    			String aword = wd.getAnglicizedWord();
    			if (length(wd.getDefinition()) >= 1) {
    				for (MEANING m : wd.getDefinition()) {
            			if (m.getMeaning().contains(aword) && !aword.equals(m.getMeaning())) {
                			w = "<" + wli.getShortName() + "> " + wd.getWord();
            				includedInMeaning.add(w);
                			if (w.length() > maxLen)
                				maxLen = w.length();
                			continue lp;
            			}
    				}
    			}
        		switch (wd.getDefinition().size()) {
        		case 1:
        			MEANING meaning = wd.getDefinition().get(0);
        			if (isNotEmpty(meaning.getExamples()) || !aword.equals(meaning.getMeaning()))
        				break;
        			// fall thru
        		case 0:
        			w = "<" + wli.getShortName() + "> " + wd.getWord();
        			withIdenticalMeaning.add(w);
        			if (w.length() > maxLen)
        				maxLen = w.length();
        			break;
        		}
    		}
    	}

    	String fmt = "%-" + (maxLen + 1) + 's';
    	int i, cols = (getPageWidth() - 4) / (maxLen + 1);
    	if (length(withIdenticalMeaning) > 0) {
        	print("Words with identical meaning and spelling:");
        	i = 0;
        	for (String s : withIdenticalMeaning) {
        		if (i++ %cols == 0)
        			print(NL + "    ");
        		printf(fmt, s);
        	}
        	println(NL);
    	}

    	if (length(includedInMeaning) > 0) {
        	print("Words that appear in their definitions:");
        	i = 0;
        	for (String s : includedInMeaning) {
        		if (i++ %cols == 0)
        			print(NL + "    ");
        		printf(fmt, s);
        	}
        	println(NL);
    	}
    }

    public String getLatestWordsFilename() {
    	return LATEST_WORDS_FILENAME + getLanguage().name() + ".log";
    }

    public void loadLatest() {
    	if (!latestWords.isEmpty())
    		return;

    	LineNumberReader lnr = null;
    	try {
    		lnr = openFile(new File(getLatestWordsFilename()));
    		String line;
    		while ((line = lnr.readLine()) != null) {
    			line = line.trim();
    			if (line.length() > 0 && !latestWordsSet.contains(line)) {
    				latestWordsSet.add(line);
    				latestWords.add(line);
    			}
    		}
    		numPrevLatestWords = latestWords.size();
		} catch(Exception e) {
		} finally {
			try { lnr.close(); } catch(Exception e) {}
		}
    }

    public void saveLatest() {
		PrintWriter pw = null;
		try {
			pw = CommonUtils.openFileToWrite(new File(getLatestWordsFilename()), true);
			for (int i=numPrevLatestWords; i<latestWords.size(); ++i)
				pw.println(latestWords.get(i));
		} catch(Exception e) {
			getPrintable().error(e);
		} finally {
			try { pw.close(); } catch(Exception e) {}
		}
    }

    public void addLatestWord(String word) {
    	if (!latestWordsSet.contains(word)) {
    		latestWords.add(word);
    		latestWordsSet.add(word);
    	}
    }

    public List<String> getLatestWords() { return latestWords; }
    public Set<String> getLatestWordsSet() { return latestWordsSet; }

    public void showLatest(boolean sort, boolean simple) {
    	String[] sa = latestWords.toArray(new String[latestWords.size()]);
    	if (sort)
    		Arrays.sort(sa);
		if (simple) {
			for (String s : sa)
				println(s);
			println();
		} else {
			getWortUbung().printToPageWidth(sa, 2);
		}
    }

    public void editLists(String[] params) {
	    Map<String, Set<String>> map = getWordListsForShortNames(params);
	    Set<String> shortNames = new HashSet<String>();
	    for (Set<String> val : map.values())
	    	shortNames.addAll(val);

		for (String sn : params) {
			if (sn.length() == 2 && Character.isDigit(sn.charAt(0)))
				shortNames.add(sn);
		}

		Runtime runtime = Runtime.getRuntime();
	    for (WordListInfo wli : getWordLists()) {
	    	if (!wli.isBuiltin() && shortNames.contains(wli.getShortName())) {
    		    try {
    		    	runtime.exec(getWortUbung().getEditorExec() + ' ' + wli.getFile().getAbsolutePath());
    		    } catch (Exception e) {
    	    		error(e);
    		    }
	    	}
	    }
    }

	////////////////////////////////////////////////////////////////////////////////////////
    // Reverse conjugation

    protected HashMap<String, List<ReverseConjugationElement>> conjugatedMap = new HashMap<String, List<ReverseConjugationElement>>();
	protected HashMap<String, VerbConjugation> preconjugatedVerbs = new HashMap<String, VerbConjugation>();
	protected HashSet<String> failedConjugations = new HashSet<String>();

	protected void preconjugate(String verb) {
		if (preconjugatedVerbs.containsKey(verb))
			return;

		PERSON[] persons = PERSON.values();

		VerbConjugation vc = null;
		try {
			vc = Conjugueur.conjugate(verb);
			preconjugatedVerbs.put(verb, vc);
		} catch(UnknownConjugationException uce) {
			failedConjugations.add(verb);
			return;
		}

		for (ConjugationSet cs : vc.getConjugationResult(null).getConjugations()) {
			String x = cs.getPastParticiple();
			if (x != null) {
    			String[] pPasse = x.split("\\|");
    			addPreconjugatedWord(pPasse[0], ReverseConjugationElement.participePasse(verb, true));
    			addPreconjugatedWord((pPasse.length == 2) ? pPasse[1] : (pPasse[0] + 'e'), ReverseConjugationElement.participePasse(verb, false));
			}
			x = cs.getPresentParticiple();
			if (x != null)
				addPreconjugatedWord(x, ReverseConjugationElement.participePresent(verb));
			for (CONJUGATION ct : CONJUGATION.forLanguage(LANG.fr)) {
				String[] conjs = cs.get(ct);
				if (conjs == null)
					continue;
				int i=0;
				for (String c : conjs) {
					if (c != null)
						addPreconjugatedWord(verb, ct, persons[i], c);
					++i;
				}
			}
		}
	}

	private void addPreconjugatedWord(String verb, CONJUGATION type, PERSON person, String conjForPerson) {
		List<ReverseConjugationElement> elems = conjugatedMap.get(conjForPerson);
		if (elems == null) {
			conjugatedMap.put(conjForPerson, elems = new ArrayList<ReverseConjugationElement>());
		} else {
			for (ReverseConjugationElement el : elems) {
				if (el.getType() == type) {
					// append the person to the existing one, and we are done
					el.addPerson(person);
					return;
				}
			}
		}
		elems.add(new ReverseConjugationElement(verb, type, person));
	}

	private void addPreconjugatedWord(String conj, ReverseConjugationElement ce) {
		List<ReverseConjugationElement> list = conjugatedMap.get(conj);
		if (list == null) {
			list = new ArrayList<ReverseConjugationElement>();
			conjugatedMap.put(conj,  list);
		}
		list.add(ce);
	}

    protected void printSpecialConjugations(Collection<VerbConjugation> conjs, Collection<String> verbsToExclude) {
    	ArrayList<String> list = new ArrayList<String>();
    	int maxLen = 6;
    	for (VerbConjugation sc : conjs) {
    		String verb = sc.getVerb();
    		if (verbsToExclude != null && verbsToExclude.contains(verb))
    			continue;
    		if (sc.isSuffixOnly())
    			verb = '_' + verb;
    		else {
    			String root = sc.getDerivedFrom();
    			if (root != null) {
    				char sep = sc.isRootSuffixOnly() ? '_' : ',';
    				verb = sc.getPrefix() + sep + root;
    			}
    		}
    		String tmp = neverNull(sc.getRefIndex2()) + '|' + ConjugationResult.getRefName(sc.getRefIndex1()) + '|' + verb + '|';
    		if (sc.hasAlternative())
    			tmp += "(2)";
    		if (verb.length() > maxLen)
    			maxLen = verb.length();
    		list.add(tmp);
    	}
    	String[] verbs = list.toArray(new String[list.size()]);
    	Arrays.sort(verbs);
    	String fmt = "  %3s  %2s   %" + maxLen + "s %3s";
    	for (String str : verbs) {
    		String[] sa = str.split("\\|");
    		printfln(fmt, sa[1], sa[0], sa[2], nn(sa, 3));
    	}
    	println();
    }

    public void listSpecialConjugations() {
    	String langName = getLangName();

    	println("List all irregular " + langName + " verbes:\n");
    	if (getLanguage() == LANG.fr) {
	    	println("    (Index 1: Harper Collins Robert FRENCH COLLEGE DICTIONARY 2nd Ed)");
	    	println("    (Index 2: French-Chinese Dictionary)\n");
    	}

    	println("Embedded irregular " + langName + " verbs:\n");
    	printSpecialConjugations(Conjugueur.defaultConjugator.getIrregularConjugations(true), null);

    	println("Preconjugated " + langName + " verbs:\n");
    	if (isNotEmpty(preconjugatedVerbs))
    		printSpecialConjugations(preconjugatedVerbs.values(), getVerbsOfIrregularConjugations());

        if (isNotEmpty(failedConjugations)) {
	   		println("Failed pre-conjugations:\n");
	   		int maxWidth = maxLen(0, failedConjugations);
	   		String fmt = "  %" + maxWidth + "s";
	   		for (String s : failedConjugations)
	   			printfln(fmt, s);
        }

        println();
    }

	////////////////////////////////////////////////////////////////////////////////////////
	// Print support
	
    public void printConjugationsJSON(ArrayList<VerbConjugation> conjs) {
		print(conjugationSuiteToJSON(conjs.toArray(new VerbConjugation[conjs.size()]), getLanguage()));
	}

	public String conjugationSuiteToJSON(VerbConjugation[] conjs, LANG lang) {
		CONJUGATION[] conjTypes = CONJUGATION.forLanguage(lang);
		int titleLen = max(maxLen(JSONConsts.class), CONJUGATION.maxNameLength(lang));
		int[] lens = { 5, 5, 5, 5, 5, 5 };

		// Prepare the widths and formats
		ConjugationResult[] results = new ConjugationResult[conjs.length];
		for (int i=0; i<results.length; ++i)
			results[i] = conjs[i].getConjugationResult(lang);
		for (ConjugationResult cr : results) {
			checkLen(lens, cr.getVerb(), 0);
			for (ConjugationSet cs : cr.getConjugations()) {
        		checkLen(lens, cs.getPresentParticiple(), 1);
        		checkLen(lens, cs.getPastParticiple(), 2);
        		for (CONJUGATION type : conjTypes) {
        			String[] sa = cs.get(type);
        			int len = length(sa);
        			for (int i=0; i<len; ++i)
        				checkLen(lens, sa[i], i);
        		}
			}
		}
		String fmt1 = toFormat(titleLen, lens, 6, true);
		String fmtL = toFormat(titleLen, true, true);
		String fmtR = toFormat(titleLen, false, true);
		String fmtC = toFormat(titleLen, true, false);

		StringBuilder sb = new StringBuilder();
		sb.append("[\n\n");
		for (ConjugationResult cr : results) {
    		sb.append("{\n");
    		if (cr.isVerified())
    			appendf(sb, fmtL, JSONConsts.IS_VERIFIED, "true");
    		sb.append(String.format(fmtL, JSONConsts.VERB, qs(cr.getVerb())));
    		if (cr.hasRefIndices()) {
    			String x = String.valueOf(cr.getRefIndex1());
    			if (cr.getRefIndex2() != null)
    				x += ", " + cr.getRefIndex2();
    			appendf(sb, fmtL, JSONConsts.REF_INDICES, "[ " + x + " ]");
    		}
    		if (isNotEmpty(cr.getDerivedFrom()))
    			appendf(sb, fmtL, JSONConsts.DERIVED_FROM, qs(cr.getDerivedFrom()));
    		String comment = cr.getMeaning();
    		if (comment != null) {
    			appendf(sb, fmtL, JSONConsts.MEANING, qs(comment));
    		} else {
    			comment = cr.getComment();
    			if (isNotEmpty(comment))
    				appendf(sb, fmtL, JSONConsts.COMMENT, qs(comment));
    		}
    		appendf(sb, fmtC, JSONConsts.CONJUGATION);

    		if (cr.hasAlternative())
    			sb.append(" [\n");
    		for (ConjugationSet cs : cr.getConjugations()) {
    			sb.append(" {\n");
        		if (isNotEmpty(cs.getComment()))
        			appendf(sb, fmtR, JSONConsts.COMMENT, qs(cs.getComment()));
    			if (cs.hasParticiples()) {
    				String x = qs(cs.getPresentParticiple());
    				if (cs.getPastParticiple() != null)
    					x += ", " + qs(cs.getPastParticiple());
        			appendf(sb, fmtR, JSONConsts.PARTICIPLES, "[ " + x + " ]");
    			}
        		for (CONJUGATION type : conjTypes) {
        			String[] sa = cs.get(type);
        			if (isAllNull(sa))
        				continue;
        			String line = String.format(fmt1, type.name(), qs(sa, 0), qs(sa, 1), qs(sa, 2), qs(sa, 3), qs(sa, 4), qs(sa, 5));
        			sb.append(trimJSONArrayEndingNulls(line, true));
        		}
        		sb.append(" },\n");
    		}
    		if (cr.hasAlternative())
    			sb.append(" ],\n");
    		sb.append("},\n");
    	}
		sb.append("\n]");

		return sb.toString();
	}

	private static String toFormat(int titleLen, boolean left, boolean withValue) {
		return " %" + (left ? "-" : "") + titleLen + "s:" + (withValue ? " %s," : "") + "\n";
	}

	private static String toFormat(int titleLen, int[] lens, int count, boolean withComma) {
   		String fmt = " %" + titleLen + "s: [ ";
   		for (int i=0; i<count; i++) {
   			if (i > 0)
   				fmt += ", ";
   			fmt += "%-" + lens[i] + "s";
   		}
   		fmt += (count == lens.length) ? " ]" : "]";
   		return fmt + (withComma ? ",\n" : "\n");
	}

	public void checkLen(int[] lens, String str, int idx) {
		if (str != null)
			lens[idx] = max(lens[idx], 2 + toLatin(str, true).length());
	}

	public void printConjugations(ArrayList<VerbConjugation> conjs) {
		int i;
		LANG lang = getLanguage();
		CONJUGATION[] conjTypes = CONJUGATION.forLanguage(lang);
		String PARTICIPLES = CONJUGATION.getParticiplesDisplay(lang);
		String INFINITIVE  = CONJUGATION.getInfinitiveDisplay(lang);

		// Prepare the titles
		String[] titles = new String[conjTypes.length + 2];
		titles[0] = INFINITIVE + "\n" + PARTICIPLES;
		titles[1] = ""; // persons column
		for (i=0; i<conjTypes.length; ++i)
			titles[i+2] = conjTypes[i].getDisplay(lang).replaceAll(" +", "\n");

		// Calculate all the widths
		String [] psn = PERSON.getShortNames(lang);
		int[] widths = new int[titles.length];
		widths[0] = getWidth(titles[0]);
		widths[1] = maxLen(0, psn);
    	for (i=0; i<conjTypes.length; ++i)
    		widths[i+2] = conjTypes[i].getDisplayWidth(lang);

    	for (VerbConjugation conj : conjs) {
    		for (ConjugationSet cs : conj.getConjugationResult(null).getConjugations()) {
        		widths[0] = maxLen(widths[0], sa(conj.getVerb(), cs.getPastParticiple(), cs.getPresentParticiple()));
        		for (i=0; i<conjTypes.length; ++i)
        			widths[i+2] = maxLen(widths[i+2], cs.get(conjTypes[i]));
    		}
    	}

    	// Write each verb conjugation collection
		ArrayList<Matrix> list = new ArrayList<Matrix>();
    	for (VerbConjugation conj : conjs) {
    		ConjugationResult cr = conj.getConjugationResult(null);
    		ArrayList<ConjugationSet> conjSets = cr.getConjugations();
    		int numConjs = conjSets.size();
    		if (numConjs <= 0)
    			continue;
			String[][] result = new String[psn.length * numConjs + (numConjs-1)][];
			for (i=0; i<result.length; ++i)
				result[i] = new String[titles.length];
			int offset = 0;
			for (int idxSet = 0; idxSet < numConjs; ++idxSet) {
				offset += idxSet * psn.length;
				if (idxSet > 0) {
					result[offset][0] = "";
					for (i=1; i<titles.length; ++i)
						result[offset][i] = ln3(widths[i]);
					++offset;
				}

				ConjugationSet cs = conjSets.get(idxSet);
    			for (i=0; i<psn.length; ++i)
    				result[offset + i][1] = psn[i];

    			result[offset + 2][0] = neverNull(cs.getPresentParticiple());
    			result[offset + 3][0] = neverNull(cs.getPastParticiple());
        		if (idxSet == 0) {
        			result[offset + 0][0] = cr.getVerb();
        			result[offset + 1][0] = ln3(widths[0]);
        			result[offset + 5][0] = rightFlush(widths[0], conj.getPrefix() == null ? "" : ("<-" + conj.getDerivedFrom()));

        			String idx = "";
            		int group = cr.getGroup();
            		if (group > 0)
            			idx = "<" + toRomanDigit(group-1) + ">";
            		else if (conj.getRefIndex1() != null || conj.getRefIndex2() != null)
            			idx = "(" + neverNull(conj.getRefIndex1()) + ',' + neverNull(conj.getRefIndex2()) + ')';
            		result[offset + 4][0] = idx;
        		}

        		for (i=conjTypes.length-1; i>=0; --i) {
        			String[] sa = cs.get(conjTypes[i]);
        			for (int j=0; j<psn.length; ++j) {
//        				String s = at(sa, j);
//        				if (s != null)
//        					s = s.replaceAll("/", "\n");
//        				result[offset + j][i+2] = s;
        				result[offset + j][i+2] = at(sa, j);
        			}
        		}
			}

    		list.add(new Matrix(list.isEmpty() ? titles : null, result));
    	}
    	
    	printMatrix(getPrintable(), STYLE.Clear, false, true, list, widths);
    }

	public static String ln(int len)  { return makeLine('-', len); }
	public static String ln2(int len) { return makeLine('=', len); }
	public static String ln3(int len) { return makeLine('.', len); }

	public void promptln(Messages msg, Object... params) {
		final ResourceBundle rb = ResourceBundle.getBundle(RESOURCE_BUNDLE, new Locale(getLanguage().name()));
		if (length(params) != msg.getParams())
			throw new WortUbungException(
					"Message " + msg + " expects " + msg.getParams() + " parameter(s) but " + length(params) + " provided.");
		String s = rb.getString(msg.name());
		if (isEmpty(params))
			promptln(s);
		else
			promptfln(s, params);
	}

} // end of class LanguageManager.
